Mở khóa sức mạnh của vòng lặp Python. Hướng dẫn toàn diện cho các nhà phát triển toàn cầu về triển khai iterator tùy chỉnh bằng phương thức __iter__ và __next__.
Giải Mã Giao Thức Iterator của Python: Đi Sâu vào __iter__ và __next__
Vòng lặp là một trong những khái niệm cơ bản nhất trong lập trình. Trong Python, đó là cơ chế thanh lịch và hiệu quả cung cấp năng lượng cho mọi thứ, từ các vòng lặp for đơn giản đến các quy trình xử lý dữ liệu phức tạp. Bạn sử dụng nó hàng ngày khi lặp qua một danh sách, đọc các dòng từ một tệp hoặc làm việc với kết quả cơ sở dữ liệu. Nhưng bạn đã bao giờ tự hỏi điều gì đang xảy ra bên trong chưa? Làm thế nào để Python biết cách lấy mục 'tiếp theo' từ rất nhiều loại đối tượng khác nhau?
Câu trả lời nằm ở một mẫu thiết kế mạnh mẽ và thanh lịch được gọi là Giao Thức Iterator. Giao thức này là ngôn ngữ chung mà tất cả các đối tượng giống chuỗi của Python đều nói. Bằng cách hiểu và triển khai giao thức này, bạn có thể tạo các đối tượng tùy chỉnh của riêng mình hoàn toàn tương thích với các công cụ vòng lặp của Python, làm cho mã của bạn biểu cảm hơn, tiết kiệm bộ nhớ hơn và tinh túy là 'Pythonic'.
Hướng dẫn toàn diện này sẽ đưa bạn đi sâu vào giao thức iterator. Chúng ta sẽ làm sáng tỏ điều kỳ diệu đằng sau các phương thức `__iter__` và `__next__`, làm rõ sự khác biệt quan trọng giữa một iterable và một iterator, đồng thời hướng dẫn bạn xây dựng các iterator tùy chỉnh của riêng mình từ đầu. Cho dù bạn là một nhà phát triển trung cấp đang muốn hiểu sâu hơn về nội bộ của Python hay một chuyên gia đang hướng tới việc thiết kế các API phức tạp hơn, thì việc nắm vững giao thức iterator là một bước quan trọng trong hành trình của bạn.
'Tại sao': Tầm Quan Trọng và Sức Mạnh của Vòng Lặp
Trước khi đi sâu vào việc triển khai kỹ thuật, điều cần thiết là phải đánh giá cao lý do tại sao giao thức iterator lại quan trọng đến vậy. Lợi ích của nó vượt xa việc chỉ kích hoạt các vòng lặp `for`.
Hiệu Quả Bộ Nhớ và Đánh Giá Trì Hoãn
Hãy tưởng tượng bạn cần xử lý một tệp nhật ký lớn có kích thước vài gigabyte. Nếu bạn đọc toàn bộ tệp vào một danh sách trong bộ nhớ, bạn có thể cạn kiệt tài nguyên hệ thống của mình. Các iterator giải quyết vấn đề này một cách tuyệt vời thông qua một khái niệm gọi là đánh giá trì hoãn.
Một iterator không tải tất cả dữ liệu cùng một lúc. Thay vào đó, nó tạo hoặc tìm nạp từng mục một, chỉ khi nó được yêu cầu. Nó duy trì một trạng thái bên trong để ghi nhớ vị trí của nó trong chuỗi. Điều này có nghĩa là bạn có thể xử lý một luồng dữ liệu lớn vô hạn (về lý thuyết) với một lượng bộ nhớ nhỏ, không đổi. Đây là nguyên tắc tương tự cho phép bạn đọc một tệp lớn theo từng dòng mà không làm sập chương trình của bạn.
Mã Sạch Sẽ, Dễ Đọc và Phổ Quát
Giao thức iterator cung cấp một giao diện phổ quát để truy cập tuần tự. Vì danh sách, bộ tuple, từ điển, chuỗi, đối tượng tệp và nhiều loại khác đều tuân thủ giao thức này, bạn có thể sử dụng cùng một cú pháp—vòng lặp `for`—để làm việc với tất cả chúng. Tính đồng nhất này là nền tảng của khả năng đọc của Python.
Xem xét đoạn mã này:
Mã:
my_list = [1, 2, 3]
for item in my_list:
print(item)
my_string = "abc"
for char in my_string:
print(char)
with open('my_file.txt', 'r') as f:
for line in f:
print(line)
Vòng lặp `for` không quan tâm nếu nó đang lặp lại trên một danh sách các số nguyên, một chuỗi các ký tự hoặc các dòng từ một tệp. Nó chỉ đơn giản là yêu cầu đối tượng iterator của nó và sau đó liên tục yêu cầu iterator mục tiếp theo của nó. Sự trừu tượng này là vô cùng mạnh mẽ.
Phân Tích Giao Thức Iterator
Bản thân giao thức này đơn giản một cách đáng ngạc nhiên, được xác định chỉ bằng hai phương thức đặc biệt, thường được gọi là phương thức "dunder" (dấu gạch dưới kép):
- `__iter__()`
- `__next__()`
Để nắm bắt đầy đủ những điều này, trước tiên chúng ta phải hiểu sự khác biệt giữa hai khái niệm liên quan nhưng khác nhau: một iterable và một iterator.
Iterable so với Iterator: Một Sự Khác Biệt Quan Trọng
Đây thường là một điểm gây nhầm lẫn cho những người mới đến, nhưng sự khác biệt là rất quan trọng.
Iterable là gì?
Một iterable là bất kỳ đối tượng nào có thể được lặp lại. Đó là một đối tượng mà bạn có thể chuyển đến hàm `iter()` tích hợp để lấy một iterator. Về mặt kỹ thuật, một đối tượng được coi là iterable nếu nó triển khai phương thức `__iter__`. Mục đích duy nhất của phương thức `__iter__` của nó là trả về một đối tượng iterator.
Ví dụ về các iterable tích hợp bao gồm:
- Danh sách (`[1, 2, 3]`)
- Bộ tuple (`(1, 2, 3)`)
- Chuỗi (`"hello"`)
- Từ điển (`{'a': 1, 'b': 2}` - lặp lại trên các khóa)
- Tập hợp (`{1, 2, 3}`)
- Đối tượng tệp
Bạn có thể nghĩ về một iterable như một vùng chứa hoặc một nguồn dữ liệu. Nó không biết cách tự tạo ra các mục, nhưng nó biết cách tạo một đối tượng có thể: iterator.
Iterator là gì?
Một iterator là đối tượng thực sự thực hiện công việc tạo ra các giá trị trong quá trình lặp. Nó đại diện cho một luồng dữ liệu. Một iterator phải triển khai hai phương thức:
- `__iter__()`: Phương thức này sẽ trả về chính đối tượng iterator (`self`). Điều này là bắt buộc để iterator cũng có thể được sử dụng ở những nơi mà iterable được mong đợi, ví dụ: trong một vòng lặp `for`.
- `__next__()`: Phương thức này là động cơ của iterator. Nó trả về mục tiếp theo trong chuỗi. Khi không còn mục nào để trả về, nó phải tạo ra ngoại lệ `StopIteration`. Ngoại lệ này không phải là một lỗi; đó là tín hiệu tiêu chuẩn cho cấu trúc vòng lặp rằng quá trình lặp đã hoàn tất.
Các đặc điểm chính của một iterator là:
- Nó duy trì trạng thái: Một iterator ghi nhớ vị trí hiện tại của nó trong chuỗi.
- Nó tạo ra các giá trị từng cái một: Thông qua phương thức `__next__`.
- Nó có thể cạn kiệt: Sau khi một iterator đã được sử dụng hết hoàn toàn (tức là, nó đã tạo ra `StopIteration`), nó sẽ trống. Bạn không thể đặt lại hoặc sử dụng lại nó. Để lặp lại, bạn phải quay lại iterable ban đầu và lấy một iterator mới bằng cách gọi `iter()` trên nó một lần nữa.
Xây Dựng Iterator Tùy Chỉnh Đầu Tiên Của Chúng Ta: Hướng Dẫn Từng Bước
Lý thuyết rất tuyệt, nhưng cách tốt nhất để hiểu giao thức là tự xây dựng nó. Hãy tạo một lớp đơn giản hoạt động như một bộ đếm, lặp lại từ một số bắt đầu đến một giới hạn.
Ví dụ 1: Một Lớp Bộ Đếm Đơn Giản
Chúng ta sẽ tạo một lớp có tên là `CountUpTo`. Khi bạn tạo một thể hiện của nó, bạn sẽ chỉ định một số tối đa và khi bạn lặp lại nó, nó sẽ tạo ra các số từ 1 đến số tối đa đó.
Mã:
class CountUpTo:
"""Một iterator đếm từ 1 đến một số tối đa được chỉ định."""
def __init__(self, max_num):
print("Khởi tạo đối tượng CountUpTo...")
self.max_num = max_num
self.current = 0 # Điều này sẽ lưu trữ trạng thái
def __iter__(self):
print("__iter__ được gọi, trả về self...")
# Đối tượng này là iterator của chính nó, vì vậy chúng ta trả về self
return self
def __next__(self):
print("__next__ được gọi...")
if self.current < self.max_num:
self.current += 1
return self.current
else:
# Đây là phần quan trọng: báo hiệu rằng chúng ta đã hoàn thành.
print("Tạo StopIteration.")
raise StopIteration
# Cách sử dụng nó
print("Tạo đối tượng bộ đếm...")
counter = CountUpTo(3)
print("\nBắt đầu vòng lặp for...")
for number in counter:
print(f"Vòng lặp For nhận được: {number}")
Phân Tích và Giải Thích Mã
Hãy phân tích những gì xảy ra khi vòng lặp `for` chạy:
- Khởi tạo: `counter = CountUpTo(3)` tạo một thể hiện của lớp của chúng ta. Phương thức `__init__` chạy, đặt `self.max_num` thành 3 và `self.current` thành 0. Trạng thái của đối tượng của chúng ta hiện đã được khởi tạo.
- Bắt đầu Vòng lặp: Khi dòng `for number in counter:` đạt đến, Python sẽ gọi nội bộ `iter(counter)`.
- `__iter__` Được Gọi: Lệnh gọi `iter(counter)` gọi phương thức `counter.__iter__()` của chúng ta. Như bạn có thể thấy từ mã của chúng ta, phương thức này chỉ đơn giản là in một thông báo và trả về `self`. Điều này cho vòng lặp `for` biết, "Đối tượng bạn cần gọi `__next__` là tôi!"
- Vòng Lặp Bắt Đầu: Bây giờ vòng lặp `for` đã sẵn sàng. Trong mỗi lần lặp, nó sẽ gọi `next()` trên đối tượng iterator mà nó nhận được (là đối tượng `counter` của chúng ta).
- Gọi `__next__` Đầu Tiên: Phương thức `counter.__next__()` được gọi. `self.current` là 0, nhỏ hơn `self.max_num` (3). Mã tăng `self.current` lên 1 và trả về nó. Vòng lặp `for` gán giá trị này cho biến `number` và phần thân vòng lặp (`print(...)`) thực thi.
- Gọi `__next__` Thứ Hai: Vòng lặp tiếp tục. `__next__` được gọi lại. `self.current` là 1. Nó được tăng lên 2 và trả về.
- Gọi `__next__` Thứ Ba: `__next__` được gọi lại. `self.current` là 2. Nó được tăng lên 3 và trả về.
- Gọi `__next__` Cuối Cùng: `__next__` được gọi thêm một lần nữa. Bây giờ, `self.current` là 3. Điều kiện `self.current < self.max_num` là sai. Khối `else` được thực thi và `StopIteration` được tạo ra.
- Kết Thúc Vòng Lặp: Vòng lặp `for` được thiết kế để bắt ngoại lệ `StopIteration`. Khi nó làm như vậy, nó biết quá trình lặp đã kết thúc và kết thúc một cách duyên dáng. Chương trình tiếp tục thực thi bất kỳ mã nào sau vòng lặp.
Lưu ý một chi tiết quan trọng: nếu bạn cố gắng chạy vòng lặp `for` trên cùng một đối tượng `counter` một lần nữa, nó sẽ không hoạt động. Iterator đã cạn kiệt. `self.current` đã là 3, vì vậy bất kỳ lệnh gọi `__next__` tiếp theo nào sẽ ngay lập tức tạo ra `StopIteration`. Đây là một hệ quả của việc đối tượng của chúng ta là iterator của chính nó.
Các Khái Niệm Iterator Nâng Cao và Ứng Dụng Thực Tế
Bộ đếm đơn giản là một cách tuyệt vời để học, nhưng sức mạnh thực sự của giao thức iterator tỏa sáng khi được áp dụng cho các cấu trúc dữ liệu tùy chỉnh phức tạp hơn.
Vấn Đề Với Việc Kết Hợp Iterable và Iterator
Trong ví dụ `CountUpTo` của chúng ta, lớp này vừa là iterable vừa là iterator. Điều này rất đơn giản nhưng có một nhược điểm lớn: iterator kết quả có thể cạn kiệt. Khi bạn lặp lại nó, nó sẽ hoàn thành.
Mã:
counter = CountUpTo(2)
print("Lần lặp đầu tiên:")
for num in counter: print(num) # Hoạt động tốt
print("\nLần lặp thứ hai:")
for num in counter: print(num) # Không in gì cả!
Điều này xảy ra vì trạng thái (`self.current`) được lưu trữ trên chính đối tượng. Sau lần lặp đầu tiên, `self.current` là 2 và bất kỳ lệnh gọi `__next__` nào nữa sẽ chỉ tạo ra `StopIteration`. Hành vi này khác với danh sách Python tiêu chuẩn, mà bạn có thể lặp lại nhiều lần.
Một Mô Hình Mạnh Mẽ Hơn: Tách Iterable Khỏi Iterator
Để tạo các iterable có thể tái sử dụng như các bộ sưu tập tích hợp của Python, cách tốt nhất là tách hai vai trò. Đối tượng vùng chứa sẽ là iterable và nó sẽ tạo ra một đối tượng iterator mới, tươi mới mỗi khi phương thức `__iter__` của nó được gọi.
Hãy tái cấu trúc ví dụ của chúng ta thành hai lớp: `Sentence` (iterable) và `SentenceIterator` (iterator).
Mã:
class SentenceIterator:
"""Iterator chịu trách nhiệm về trạng thái và tạo ra các giá trị."""
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
# Một iterator cũng phải là một iterable, trả về chính nó.
return self
class Sentence:
"""Lớp vùng chứa iterable."""
def __init__(self, text):
# Vùng chứa chứa dữ liệu.
self.words = text.split()
def __iter__(self):
# Mỗi khi __iter__ được gọi, nó sẽ tạo một đối tượng iterator MỚI.
return SentenceIterator(self.words)
# Cách sử dụng nó
my_sentence = Sentence('Đây là một bài kiểm tra')
print("Lần lặp đầu tiên:")
for word in my_sentence:
print(word)
print("\nLần lặp thứ hai:")
for word in my_sentence:
print(word)
Bây giờ, nó hoạt động chính xác như một danh sách! Mỗi khi vòng lặp `for` bắt đầu, nó sẽ gọi `my_sentence.__iter__()`, tạo một thể hiện `SentenceIterator` hoàn toàn mới với trạng thái riêng (`self.index = 0`). Điều này cho phép lặp lại nhiều lần, độc lập trên cùng một đối tượng `Sentence`. Mô hình này mạnh mẽ hơn nhiều và là cách các bộ sưu tập của chính Python được triển khai.
Ví dụ: Iterator Vô Hạn
Iterator không cần phải hữu hạn. Chúng có thể đại diện cho một chuỗi dữ liệu vô tận. Đây là nơi mà bản chất lười biếng, từng cái một của chúng là một lợi thế rất lớn. Hãy tạo một iterator cho một chuỗi vô hạn các số Fibonacci.
Mã:
class FibonacciIterator:
"""Tạo một chuỗi vô hạn các số Fibonacci."""
def __init__(self):
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
result = self.a
self.a, self.b = self.b, self.a + self.b
return result
# Cách sử dụng nó - CẢNH BÁO: Vòng lặp vô hạn nếu không có lệnh break!
fib_gen = FibonacciIterator()
for i, num in enumerate(fib_gen):
print(f"Fibonacci({i}): {num}")
if i >= 10: # Chúng ta phải cung cấp một điều kiện dừng
break
Iterator này sẽ không bao giờ tự tạo ra `StopIteration`. Trách nhiệm của mã gọi là cung cấp một điều kiện (như một câu lệnh `break`) để kết thúc vòng lặp. Mô hình này phổ biến trong truyền dữ liệu, vòng lặp sự kiện và mô phỏng số.
Giao Thức Iterator trong Hệ Sinh Thái Python
Hiểu `__iter__` và `__next__` cho phép bạn thấy ảnh hưởng của chúng ở khắp mọi nơi trong Python. Đó là giao thức thống nhất giúp rất nhiều tính năng của Python hoạt động cùng nhau một cách liền mạch.
Cách Vòng Lặp `for` Hoạt Động *Thực Sự*
Chúng ta đã thảo luận về điều này một cách ngầm định, nhưng hãy làm cho nó rõ ràng. Khi Python gặp dòng này:
`for item in my_iterable:`
Nó thực hiện các bước sau ở hậu trường:
- Nó gọi `iter(my_iterable)` để lấy một iterator. Điều này, đến lượt nó, gọi `my_iterable.__iter__()`. Hãy gọi đối tượng được trả về là `iterator_obj`.
- Nó đi vào một vòng lặp `while True` vô hạn.
- Bên trong vòng lặp, nó gọi `next(iterator_obj)`, đến lượt nó gọi `iterator_obj.__next__()`.
- Nếu `__next__` trả về một giá trị, nó sẽ được gán cho biến `item` và mã bên trong khối vòng lặp `for` được thực thi.
- Nếu `__next__` tạo ra một ngoại lệ `StopIteration`, vòng lặp `for` sẽ bắt ngoại lệ này và thoát khỏi vòng lặp `while` bên trong của nó. Quá trình lặp đã hoàn tất.
Hiểu và Biểu Thức Generator
Các hiểu danh sách, tập hợp và từ điển đều được cung cấp bởi giao thức iterator. Khi bạn viết:
`squares = [x * x for x in range(10)]`
Python đang thực hiện hiệu quả một vòng lặp trên đối tượng `range(10)`, lấy từng giá trị và thực thi biểu thức `x * x` để xây dựng danh sách. Điều tương tự cũng đúng với các biểu thức generator, đó là việc sử dụng trực tiếp hơn nữa của quá trình lặp lại lười biếng:
`lazy_squares = (x * x for x in range(1000000))`
Điều này không tạo ra một danh sách một triệu mục trong bộ nhớ. Nó tạo ra một iterator (cụ thể là một đối tượng generator) sẽ tính toán các hình vuông từng cái một, khi bạn lặp lại nó.
Generator: Cách Đơn Giản Hơn để Tạo Iterator
Mặc dù việc tạo một lớp đầy đủ với `__iter__` và `__next__` cho bạn khả năng kiểm soát tối đa, nhưng nó có thể dài dòng đối với các trường hợp đơn giản. Python cung cấp một cú pháp ngắn gọn hơn nhiều để tạo iterator: generator.
Một generator là một hàm sử dụng từ khóa `yield`. Khi bạn gọi một hàm generator, nó không chạy mã. Thay vào đó, nó trả về một đối tượng generator, là một iterator đầy đủ.
Hãy viết lại ví dụ `CountUpTo` của chúng ta dưới dạng một generator:
Mã:
def count_up_to_generator(max_num):
"""Một hàm generator tạo ra các số từ 1 đến max_num."""
print("Generator đã bắt đầu...")
current = 1
while current <= max_num:
yield current # Tạm dừng ở đây và gửi một giá trị trở lại
current += 1
print("Generator đã hoàn thành.")
# Cách sử dụng nó
counter_gen = count_up_to_generator(3)
for number in counter_gen:
print(f"Vòng lặp For nhận được: {number}")
Hãy xem nó đơn giản hơn bao nhiêu! Từ khóa `yield` là điều kỳ diệu ở đây. Khi `yield` được gặp, trạng thái của hàm sẽ bị đóng băng, giá trị được gửi đến người gọi và hàm tạm dừng. Lần tới khi `__next__` được gọi trên đối tượng generator, hàm sẽ tiếp tục thực thi ngay tại nơi nó đã dừng lại, cho đến khi nó gặp một `yield` khác hoặc hàm kết thúc. Khi hàm kết thúc, một `StopIteration` sẽ tự động được tạo cho bạn.
Trong nội bộ, Python đã tự động tạo một đối tượng có các phương thức `__iter__` và `__next__`. Mặc dù generator thường là lựa chọn thiết thực hơn, nhưng việc hiểu giao thức cơ bản là rất cần thiết để gỡ lỗi, thiết kế các hệ thống phức tạp và đánh giá cao cách hoạt động của cơ chế cốt lõi của Python.
Các Phương Pháp Hay Nhất và Những Cạm Bẫy Phổ Biến
Khi triển khai giao thức iterator, hãy ghi nhớ các hướng dẫn này để tránh các lỗi phổ biến.
Các Phương Pháp Hay Nhất
- Tách Iterable và Iterator: Đối với bất kỳ đối tượng vùng chứa nào hỗ trợ nhiều lần duyệt, hãy luôn triển khai iterator trong một lớp riêng biệt. Phương thức `__iter__` của vùng chứa sẽ trả về một thể hiện mới của lớp iterator mỗi lần.
- Luôn Tạo Ra `StopIteration`: Phương thức `__next__` phải tạo ra `StopIteration` một cách đáng tin cậy để báo hiệu kết thúc. Quên điều này sẽ dẫn đến các vòng lặp vô hạn.
- Iterator phải là iterable: Phương thức `__iter__` của một iterator sẽ luôn trả về `self`. Điều này cho phép một iterator được sử dụng ở bất cứ đâu một iterable được mong đợi.
- Ưu Tiên Generator cho Sự Đơn Giản: Nếu logic iterator của bạn đơn giản và có thể được biểu thị dưới dạng một hàm duy nhất, thì generator hầu như luôn sạch hơn và dễ đọc hơn. Sử dụng một lớp iterator đầy đủ khi bạn cần liên kết trạng thái hoặc phương thức phức tạp hơn với chính đối tượng iterator.
Những Cạm Bẫy Phổ Biến
- Vấn Đề Iterator Có Thể Cạn Kiệt: Như đã thảo luận, hãy lưu ý rằng khi một đối tượng là iterator của chính nó, nó chỉ có thể được sử dụng một lần. Nếu bạn cần lặp lại nhiều lần, bạn phải tạo một thể hiện mới hoặc sử dụng mô hình iterable/iterator được tách biệt.
- Quên Trạng Thái: Phương thức `__next__` phải sửa đổi trạng thái bên trong của iterator (ví dụ: tăng một chỉ mục hoặc di chuyển một con trỏ). Nếu trạng thái không được cập nhật, `__next__` sẽ trả về cùng một giá trị lặp đi lặp lại, có khả năng gây ra một vòng lặp vô hạn.
- Sửa Đổi Một Bộ Sưu Tập Trong Khi Lặp: Lặp lại một bộ sưu tập trong khi sửa đổi nó (ví dụ: xóa các mục khỏi một danh sách bên trong vòng lặp `for` đang lặp lại nó) có thể dẫn đến hành vi không thể đoán trước, chẳng hạn như bỏ qua các mục hoặc tạo ra các lỗi bất ngờ. Nói chung, an toàn hơn khi lặp lại trên một bản sao của bộ sưu tập nếu bạn cần sửa đổi bản gốc.
Kết Luận
Giao thức iterator, với các phương thức `__iter__` và `__next__` đơn giản của nó, là nền tảng của vòng lặp trong Python. Nó là một minh chứng cho triết lý thiết kế của ngôn ngữ: ưu tiên các giao diện đơn giản, nhất quán cho phép các hành vi mạnh mẽ và phức tạp. Bằng cách cung cấp một hợp đồng phổ quát để truy cập dữ liệu tuần tự, giao thức cho phép các vòng lặp `for`, các hiểu và vô số công cụ khác hoạt động liền mạch với bất kỳ đối tượng nào chọn nói ngôn ngữ của nó.
Bằng cách làm chủ giao thức này, bạn đã mở khóa khả năng tạo các đối tượng giống chuỗi của riêng bạn, những đối tượng là công dân hạng nhất trong hệ sinh thái Python. Giờ đây, bạn có thể viết các lớp tiết kiệm bộ nhớ hơn bằng cách xử lý dữ liệu một cách lười biếng, trực quan hơn bằng cách tích hợp một cách sạch sẽ với cú pháp Python tiêu chuẩn và cuối cùng là mạnh mẽ hơn. Lần tới khi bạn viết một vòng lặp `for`, hãy dành một chút thời gian để đánh giá cao điệu nhảy thanh lịch của `__iter__` và `__next__` đang diễn ra ngay bên dưới bề mặt.